<?php

///////////////////////////////////////////////////////////////////////////////
/**
 * IConnector implementation for the PHP MySQL database extension.
 *
 * System requirements:
 * <ul>
 * <li>PHP 5</li>
 * <li>The {@link PHP_MANUAL#book.mysql MySQL database extension}</li>
 * </ul>
 *
 * This library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * The Connector library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * {@link http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License}
 * for more details.
 *
 * @author Per Egil Roksvaag
 * @copyright 2009 Per Egil Roksvaag
 * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
 * @package connector
 * @version 2.0.0
 */

///////////////////////////////////////////////////////////////////////////////
/**
 * Includes the parent class.
 */

require_once("BaseConnector.php");

///////////////////////////////////////////////////////////////////////////////
/**
 * IConnector implementation for the PHP MySQL database extension.
 * @package connector
 */

class MysqlConnector extends BaseConnector implements IConnector
{
	///////////////////////////////////////////////////////////////////////////
	/**
	 * List of additional escape characters for MySQL string parameters.
	 */

	const PARAM_ADD_SLASHES = "paramAddSlashes";

	///////////////////////////////////////////////////////////////////////////
	/**
	 * Open a connection to a MySQL Server and set global options.
	 *
	 * @param array $connection An associated array of connection settings, like host and user name.
	 * @param array $options An associated array of global options for the resulting instance.
	 * @return MysqlConnector
	 */

	public function MysqlConnector($connection, $options = array())
	{
		if($this->open($connection))
		{
			$this->options[self::PARAM_ADD_SLASHES] = "()+-/?@;";
			unset($this->options[self::PARAM_PREFIX]);
			unset($this->options[self::PARAM_QUERIES]);
			parent::BaseConnector($options);
		}
		else if($this->lookup("throwException", $options))
		{
			throw new Exception("MysqlConnector error: Connection failed.");
		}
		else if($this->lookup(self::LOG_ERROR, $options))
		{
			$log = "MysqlConnector error: Connection failed in ";
			error_log($log.$_SERVER["SCRIPT_FILENAME"]);
		}
	}

	///////////////////////////////////////////////////////////////////////////
	/**
	 * Open a database connection.
	 * @param array $connection An associated array of connection settings, like host and user name.
	 * @return bool <var>true</var>, if a database connection was successfully opened, <var>false</var> otherwise.
	 */

	protected function open($connection)
	{
		$hostname = $this->lookup(self::CONN_HOSTNAME, $connection);
		$database = $this->lookup(self::CONN_DATABASE, $connection);
		$username = $this->lookup(self::CONN_USERNAME, $connection);
		$password = $this->lookup(self::CONN_PASSWORD, $connection);

		$native = $this->lookup(self::CONN_NATIVE, $connection, 0);
		$charset = $this->lookup(self::CONN_CHARSET, $connection, "utf8");
		$pool = $this->lookup(self::CONN_POOL, $connection, false);
		$port = $this->lookup(self::CONN_PORT, $connection);
		$port && $hostname.= ":".$port;
		
		$this->conn = $this->lookup(self::CONN_PERSISTENT, $connection)
			? mysql_pconnect($hostname, $username, $password, $native)
			: mysql_connect($hostname, $username, $password, !$pool, $native);

		if($this->conn)
		{
			$database && mysql_select_db($database);
			$charset && mysql_query("SET NAMES '{$charset}'");
		}
		return is_resource($this->conn);
	}

	///////////////////////////////////////////////////////////////////////////
	/**
	 * Send a SQL SELECT query to the database and get the query result.
	 *
	 * @see IConnector::select().
	 * @param string $query A SQL query to execute on a database.
	 * @param array $param An associated array of values to be used in the $query.
	 * @param array $map An array of type definitions for the <var>$param</var> values.
	 * @param array $options An associated array of options, see the {@tutorial connector.pkg#options.element}.
	 * @throws Exception when $param doesn't match the type definition $map.
	 * @return array The query result as a table (array of associated arrays).
	 */

	public function select($query, $param = array(), $map = array(), $options = array())
	{
		$param = TypeValidator::check($param, $map);
		$query = $this->bind($query, $param, $stack, $options);
		$query = $this->build($query, $options);
		$hash = $this->getHash($query, $stack, $options);

		if($this->lookup(self::LOG_DEBUG, $options))
		{
			$log = "MysqlConnector debug: Select query in ";
			error_log($log.$_SERVER["SCRIPT_FILENAME"].LB.$query);
		}
		if($this->getCache($hash, $table, $options))
		{
			return $table;
		}
		if($stmt = mysql_query($query, $this->conn))
		{
			$table = $this->fetch($stmt, $options);
			mysql_free_result($stmt);
			$this->setCache($hash, $table, $options);
			return $table;
		}
		if($this->lookup(self::LOG_ERROR, $options))
		{
			$log = "MysqlConnector error: Select query in ";
			error_log($log.$_SERVER["SCRIPT_FILENAME"].LB.$query);
			error_log("MysqlConnector error: ".mysql_error($this->conn));
		}
		return false;
	}

	///////////////////////////////////////////////////////////////////////////
	/**
	 * Send a SQL INSERT query to the database and get the IDENTITY ID
	 * generated from the last INSERT operation (if any).
	 *
	 * @see IConnector::insert().
	 * @param string $query A SQL query to execute on a database.
	 * @param array $param An associated array of values to be used in the $query.
	 * @param array $map An array of type definitions for the <var>$param</var> values.
	 * @param array $options An associated array of options, see the {@tutorial connector.pkg#options.element}.
	 * @throws Exception when $param doesn't match the type definition $map.
	 * @return int The IDENTITY ID of the last inserted row.
	 */

	public function insert($query, $param = array(), $map = array(), $options = array())
	{
		$param = TypeValidator::check($param, $map);
		$query = $this->bind($query, $param, $stack, $options);
		$query = rtrim($query, ";");

		if($this->lookup(self::LOG_DEBUG, $options))
		{
			$log = "MysqlConnector debug: Insert query in ";
			error_log($log.$_SERVER["SCRIPT_FILENAME"].LB.$query);
		}
		if(mysql_query($query, $this->conn))
		{
			$key = mysql_insert_id($this->conn);
			return ($key !== 0) ? $key : null;
 		}
		else if($this->lookup(self::LOG_ERROR, $options))
		{
			$log = "MysqlConnector error: Insert query in ";
			error_log($log.$_SERVER["SCRIPT_FILENAME"].LB.$query);
			error_log("MysqlConnector error: ".mysql_error($this->conn));
		}
		return false;
	}

	///////////////////////////////////////////////////////////////////////////
	/**
	 * Send a SQL UPDATE query to the database and get the number of rows
	 * updates by the query.
	 *
	 * @see IConnector::update().
	 * @param string $query A SQL query to execute on a database.
	 * @param array $param An associated array of values to be used in the $query.
	 * @param array $map An array of type definitions for the <var>$param</var> values.
	 * @param array $options An associated array of options, see the {@tutorial connector.pkg#options.element}.
	 * @throws Exception when $param doesn't match the type definition $map.
	 * @return int The number of rows updates by the query.
	 */

	public function update($query, $param = array(), $map = array(), $options = array())
	{
		$param = TypeValidator::check($param, $map);
		$query = $this->bind($query, $param, $stack, $options);
		$query = rtrim($query, ";");

		if($this->lookup(self::LOG_DEBUG, $options))
		{
			$log = "MysqlConnector debug: Update query in ";
			error_log($log.$_SERVER["SCRIPT_FILENAME"].LB.$query);
		}
		if(mysql_query($query, $this->conn))
		{
			return mysql_affected_rows($this->conn);
 		}
		else if($this->lookup(self::LOG_ERROR, $options))
		{
			$log = "MysqlConnector error: Update query in ";
			error_log($log.$_SERVER["SCRIPT_FILENAME"].LB.$query);
			error_log("MysqlConnector error: ".mysql_error($this->conn));
		}
		return false;
	}

	///////////////////////////////////////////////////////////////////////////
	/**
	 * Send a SQL DELETE query to the database and get the number of rows
	 * deleted by the query.
	 *
	 * @see IConnector::delete().
	 * @param string $query A SQL query to execute on a database.
	 * @param array $param An associated array of values to be used in the $query.
	 * @param array $map An array of type definitions for the <var>$param</var> values.
	 * @param array $options An associated array of options, see the {@tutorial connector.pkg#options.element}.
	 * @throws Exception when $param doesn't match the type definition $map.
	 * @return int The number of rows deleted by the query.
	 */

	public function delete($query, $param = array(), $map = array(), $options = array())
	{
		$param = TypeValidator::check($param, $map);
		$query = $this->bind($query, $param, $stack, $options);
		$query = rtrim($query, ";");

		if($this->lookup(self::LOG_DEBUG, $options))
		{
			$log = "MysqlConnector debug: Delete query in ";
			error_log($log.$_SERVER["SCRIPT_FILENAME"].LB.$query);
		}
		if(mysql_query($query, $this->conn))
		{
			return mysql_affected_rows($this->conn);
 		}
		else if($this->lookup(self::LOG_ERROR, $options))
		{
			$log = "MysqlConnector error: Delete query in ";
			error_log($log.$_SERVER["SCRIPT_FILENAME"].LB.$query);
			error_log("MysqlConnector error: ".mysql_error($this->conn));
		}
		return false;
	}

	///////////////////////////////////////////////////////////////////////////
	/**
	 * Escape special characters in a string parameter.
	 *
	 * @param string $value The string to be escaped.
	 * @param array $options An associated array of options.
	 * @return string The escaped string.
	 */

	protected function strEscape($value, $options = array())
	{
		$slash = $this->lookup(self::PARAM_ADD_SLASHES, $options);
		$value = mysql_real_escape_string($value, $this->conn);
		return $slash ? addcslashes($value, $slash) : $value;
	}

	///////////////////////////////////////////////////////////////////////////
	/**
	 * Add a LIMIT x y statement to a SQL SELECT query when the
	 * {@link IConnector::RESULT_LENGTH} or {@link IConnector::RESULT_OFFSET}
	 * is set.
	 *
	 * @param string $query A SQL query to execute on a database.
	 * @param array $options An associated array of options.
	 * @return string A modified SQL query.
	 */

	protected function build($query, $options = array())
	{
		$query = rtrim($query, ";");
		$offset = $this->lookup(self::RESULT_OFFSET, $options, 0);
		$length = $this->lookup(self::RESULT_LENGTH, $options);
				
		if($offset || is_int($length))
		{
			if(is_null($length)) $length = PHP_INT_MAX;
			$query.= LB."LIMIT  {$offset}, {$length}";
		}
		return $query;
	}

	///////////////////////////////////////////////////////////////////////////
	/**
	 * Fetch multiple rows of a query result.
	 *
	 * If the {@link IConnector::RESULT_LENGTH} or {@link IConnector::RESULT_OFFSET}
	 * options are set, some rows are omitted from the beginning and/or the end
	 * of the query result.
	 * If the {@link IConnector::RESULT_KEY_FIELD} option is set, the
	 * resulting table is an <b>associated</b> array of rows.
	 *
	 * @param resource $stmt A statement resource corresponding to an executed statement.
	 * @param array $options An associated array of options.
	 * @return array The query result as a table (array of associated arrays).
	 */

	protected function fetch($stmt, $options = array())
	{
		$key = $this->lookup(self::RESULT_KEY_FIELD, $options);
		$table = array();

		while($row = mysql_fetch_array($stmt, MYSQL_ASSOC))
		{
			$row = $this->strDecode($row);
			$key ? $table[$row[$key]] = $row : $table[] = $row;
		}
		return $table;
	}

	///////////////////////////////////////////////////////////////////////////
}

?>